跳到主要内容

SpringCloud Hystrix 熔断器

参考资料 防雪崩利器:熔断器 Hystrix 的原理与使用 参考资料 什么是服务雪崩? 参考资料 白话:服务降级与熔断的区别 参考资料 Hystrix熔断器技术解析-HystrixCircuitBreaker

熔断是什么?

服务熔断的作用类似于家用的保险丝,当某服务出现不可用或响应超时的情况时,为了防止整个系统出现雪崩,暂时停止对该服务的调用。

在 Spring Cloud 框架里,熔断机制通过 Hystrix 实现。Hystrix 会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是 @HystrixCommand

服务降级与服务熔断区别

这里转载大佬的博客原文 白话:服务降级与熔断的区别

下面通过一个日常的故事来说明一下什么是服务降级,什么是熔断。

故事的背景是这样的:由于小强在工作中碰到一些问题,于是想请教一下业界大牛小壮。于是发生了下面的两个场景:

小强在拿起常用手机拨号时发现该手机没有能够拨通,所以就拿出了备用手机拨通了某A的电话,这个过程就叫做 降级(主逻辑失败采用备用逻辑的过程)。

由于每次小壮的解释都属于长篇大论,不太容易理解,所以小强每次找小壮沟通的时候都希望通过常用手机来完成,因为该手机有录音功能,这样自己可以慢慢消化。

由于上一次的沟通是用备用电话完成的,小强又碰到了一些问题,于是他又尝试用常用电话拨打,这一次又没有能够拨通,所以他不得不又拿出备用手机给某A拨号,就这样连续的经过了几次在拨号设备选择上的“降级”,小强觉得短期内常用手机可能因为运营商问题无法正常拨通了,所以,再之后一段时间的交流中,小强就不再尝试用常用手机进行拨号,而是直接用备用手机进行拨号,这样的策略就是 熔断(常用手机因短期内多次失败,而被暂时性的忽略,不再尝试使用)。

总结:

服务熔断是应对系统服务雪崩的一种保险措施,给出的一种特殊降级措施。而服务降级则是更加宽泛的概念,主要是对系统整体资源的合理分配以应对压力。

服务熔断是服务降级的一种特殊情况,他是防止服务雪崩而采取的措施。系统发生异常或者延迟或者流量太大,都会触发该服务的服务熔断措施,链路熔断,返回兜底方法。这是对局部的一种保险措施。

服务降级是对系统整体资源的合理分配。区分核心服务和非核心服务。对某个服务的访问延迟时间、异常等情况做出预估并给出兜底方法。这是一种全局性的考量,对系统整体负荷进行管理。

熔断器工作原理

熔断器模式定义了熔断器开关相互转换的逻辑:

image.png

服务的健康状况 = 请求失败数 / 请求总数(就是求百分比)

熔断器开关由关闭到打开的状态转换是通过当前服务健康状况和设定阈值比较决定的

Hystrix的内部处理逻辑:

1 关闭(Closed):当熔断器开关关闭时,请求被允许通过熔断器。如果当前健康状况高于设定阈值,开关继续保持关闭。如果当前健康状况低于设定阈值,开关则切换为打开状态。

2 打开(Open):当熔断器开关打开时,请求被禁止通过。

3 半开(Half Open):当熔断器开关处于打开状态,经过一段时间后,熔断器会自动进入半开状态,这时熔断器只允许一个请求通过。当该请求调用成功时,熔断器恢复到关闭状态。若该请求失败,熔断器继续保持打开状态,接下来的请求被禁止通过。

熔断器的开关能保证服务调用者在调用异常服务时,快速返回结果,避免大量的同步等待。并且熔断器能在一段时间后继续侦测请求执行结果,提供恢复服务调用的可能。

简单配置使用

这里简单演示一下熔断器怎么使用,具体配置参数看下面那节

//服务熔断
@HystrixCommand(fallbackMethod = "paymentCircuitBreakerFallback", commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"), //是否开启断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), //请求次数
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), //时间窗口期
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"), //失败率达到多少后跳闸
})
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
if (id < 0) {
throw new RuntimeException("id 不能为负数"); // 这里抛出运行时错误
}

// IdUtil.simpleUUID() 等价于 UUID.randomUUID().toString(true);
String serialNumber = IdUtil.simpleUUID();
return Thread.currentThread().getName() + "\t" + "调用成功,流水号:" + serialNumber;
}


public String paymentCircuitBreakerFallback(@PathVariable("id") Integer id) {
return "当前请求的 ID 为" + id + " 错误:ID 不能为负";
}

断路器的三个重要参数

涉及到断路器的三个重要参数:快照时间窗、请求总数阀值、错误百分比阀值。

快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。

请求总数阀值:在快照时间窗内,必须满足请求总数阀值才有资格熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开。

错误百分比阀值:当请求总数在快照时间窗内超过了阀值,比如发生了30次调用,如果在这30次调用中,有15次发生了超时异常,也就是超过 50%的错误百分比,在默认设定50%阀值情况下,这时候就会将断路器打开。

如何使用熔断器(Circuit Breaker)

这里直接转载自这篇 Hystrix熔断器技术解析-HystrixCircuitBreaker 配置详情参考自 官方配置文档

由于 Hystrix 是一个容错框架,因此我们在使用的时候,要达到熔断的目的只需配置一些参数就可以了。但我们要达到真正的效果,就必须要了解这些参数。Circuit Breaker一共包括如下6个参数。

1、circuitBreaker.enabled 是否启用熔断器,默认是 true

2、circuitBreaker.forceOpen 熔断器强制打开,始终保持打开状态。默认值 false

3、circuitBreaker.forceClosed 熔断器强制关闭,始终保持关闭状态。默认值false

4、circuitBreaker.errorThresholdPercentage 设定错误百分比,默认值50%,例如一段时间(10s)内有100个请求,其中有55个超时或者异常返回了,那么这段时间内的错误百分比是55%,大于了默认值50%,这种情况下触发熔断器-打开。

5、circuitBreaker.requestVolumeThreshold 默认值20,意思是至少有20个请求才进行 errorThresholdPercentage 错误百分比计算。比如一段时间(10s)内有19个请求全部失败了。错误百分比是100%,但熔断器不会打开,因为 requestVolumeThreshold 的值是20。这个参数非常重要,熔断器是否打开首先要满足这个条件,源代码如下

// check if we are past the statisticalWindowVolumeThreshold
if (health.getTotalRequests() < properties.circuitBreakerRequestVolumeThreshold().get()) {
// we are not past the minimum volume threshold for the statisticalWindow so we'll return false immediately and not calculate anything
return false;
}

if (health.getErrorPercentage() < properties.circuitBreakerErrorThresholdPercentage().get()) {
return false;
}

6、circuitBreaker.sleepWindowInMilliseconds 半开试探休眠时间,默认值5000ms。当熔断器开启一段时间之后比如5000ms,会尝试放过去一部分流量进行试探,确定依赖服务是否恢复。

配置熔断器属性

配置详情参考自 官方配置文档

注意:下面的请求都是 10 秒之内的

如果使用继承 HystrixCommand 接口的方式来配置,则是使用下面这些方法

1、熔断器的开关(默认开启)

// name=circuitBreaker.enabled
HystrixCommandProperties.Setter()
.withCircuitBreakerEnabled(boolean value)

2、使熔断的最小请求数(默认是 20) 例如:设置该值为 20,即在设置的滚动时间内即使 19 个请求都失败了也不会熔断

// name=circuitBreaker.requestVolumeThreshold
HystrixCommandProperties.Setter()
.withCircuitBreakerRequestVolumeThreshold(int value)

3、设置请求总数失败率大于多少时会使之熔断(默认是 50%)

// name=circuitBreaker.errorThresholdPercentage
HystrixCommandProperties.Setter()
.withCircuitBreakerErrorThresholdPercentage(int value)

4、设置要过多少毫秒会重新回到半开状态(默认是 5000)

// name=circuitBreaker.sleepWindowInMilliseconds
HystrixCommandProperties.Setter()
.withCircuitBreakerSleepWindowInMilliseconds(int value)

测试代码(模拟10次调用,错误百分比为5%的情况下,打开熔断器开关):

这里采用实现 HystrixCommand 接口的方式来配置(一般是使用上面那种 @HystrixProperty 注解配置)

public class GetOrderCircuitBreakerCommand extends HystrixCommand<String> {

public GetOrderCircuitBreakerCommand(String name){
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ThreadPoolTestGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("testCommandKey"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey(name))
.andCommandPropertiesDefaults(
HystrixCommandProperties.Setter()
.withCircuitBreakerEnabled(true)//默认是true,本例中为了展现该参数
.withCircuitBreakerForceOpen(false)//默认是false,本例中为了展现该参数
.withCircuitBreakerForceClosed(false)//默认是false,本例中为了展现该参数
.withCircuitBreakerErrorThresholdPercentage(5)//(1)错误百分比超过5%
.withCircuitBreakerRequestVolumeThreshold(10)//(2)10s以内调用次数10次,同时满足(1)(2)熔断器打开
.withCircuitBreakerSleepWindowInMilliseconds(5000)//隔5s之后,熔断器会尝试半开(关闭),重新放进来请求
)
.andThreadPoolPropertiesDefaults(
HystrixThreadPoolProperties.Setter()
.withMaxQueueSize(10) //配置队列大小
.withCoreSize(2) // 配置线程池里的线程数
)
);
}

@Override
protected String run() throws Exception {
Random rand = new Random();
//模拟错误百分比(方式比较粗鲁但可以证明问题)
if(1 == rand.nextInt(2)){
throw new Exception("make exception");
}
return "running: ";
}

@Override
protected String getFallback() {
return "fallback: ";
}

public static class UnitTest{

@Test
public void testCircuitBreaker() throws Exception{
for(int i=0; i<25; i++){
Thread.sleep(500);
HystrixCommand<String> command = new GetOrderCircuitBreakerCommand("testCircuitBreaker");
String result = command.execute();

//本例子中从第11次,熔断器开始打开
System.out.println("call times:"+(i+1)+" result:"+result +" isCircuitBreakerOpen: "+command.isCircuitBreakerOpen());
//本例子中5s以后,熔断器尝试关闭,放开新的请求进来
}
}
}
}